Skip to content

feat: move plugin loading to dedicated hidden BrowserWindow (Phase 1)#9889

Merged
jackkav merged 39 commits into
Kong:developfrom
jackkav:chore/move-plugins-to-hidden-window
May 20, 2026
Merged

feat: move plugin loading to dedicated hidden BrowserWindow (Phase 1)#9889
jackkav merged 39 commits into
Kong:developfrom
jackkav:chore/move-plugins-to-hidden-window

Conversation

@jackkav
Copy link
Copy Markdown
Contributor

@jackkav jackkav commented May 4, 2026

Summary

Moves all plugin loading and execution out of the UI renderer into a dedicated hidden `BrowserWindow` with `nodeIntegration: true`. The renderer communicates with the plugin window exclusively over IPC.

What changed

  • New hidden plugin window (`src/plugin-window.html` + `src/entry.plugin-window.ts`) — loads and executes all plugin code with Node.js access
  • Preload for plugin window (`src/entry.plugin-window-preload.ts`) — provides `window.app.getPath` via `ipcRenderer.sendSync` so plugins can resolve `userData`
  • Main process manager (`src/main/plugin-window.ts`) — creates the window; routes IPC via a pending-request map (UUID correlation, 10s ready timeout, sender validation, crash drain on close)
  • IPC database proxy (`src/main/database.plugin-window.ts`) — plugin window reads from the main process NeDB connection via IPC instead of opening a second connection; services are initialized before the window signals readiness
  • Serializable type boundary (`src/plugins/bridge-types.ts`) — all data crossing IPC is serializable; context modules run in the plugin window, not the renderer
  • Renderer bridge (`window.main.plugins.*`) — all plugin execution replaced with bridge calls
  • Build — two new esbuild entry points added; dev `buildCount` threshold updated from 3 → 6

Architecture

Renderer
  window.main.plugins.executeAction({ type, pluginName, label, projectId, domainData })
    └─ ipcRenderer.invoke('plugins.executeAction', args)
         └─ ipcMain.handle → invokeInPluginWindow('executeAction', args)
              └─ pluginWindow.webContents.send('plugin-invoke', { id, method, args })
                   └─ entry.plugin-window.ts: finds action, builds context, calls action(ctx, domainData)
                        └─ ipcRenderer.send('plugin-invoke-result', { id, result })
                             └─ main pendingRequests.get(id).resolve(result)
                                  └─ renderer Promise resolves

What's bridged (Phase 1 complete)

Feature Status
Theme listing
Plugin listing / reload
Bundle plugin listing (`getBundlePlugins`)
Request / RequestGroup / Workspace / Document actions (list + execute)
Template tag listing (`getTemplateTags`)
Template tag action execution (`runTemplateTagAction`)
Elevated plugin main actions (`executePluginMainAction`)
Request hooks / Response hooks
Sandbox hardening ⏳ Phase 2 — `contextIsolation: true` once API surface is stable

Hook bridging design

Request and response hooks mutate their context objects by reference, which only works within a single process. To cross the IPC boundary the plugin window receives a serialized copy of the request/response, runs all registered hooks against it, and returns the fully mutated object — matching the same pattern used by pre-request scripts.

The built-in default-headers hook runs in the renderer without any IPC (it has no plugin dependency). A `hasRequestHooks`/`hasResponseHooks` result is cached in the main process after the first check and cleared on `reloadPlugins`, so requests incur zero plugin-window round-trips when no user plugins have hooks registered.

Non-renderer process handling

`_applyRequestPluginHooks` and `_applyResponsePluginHooks` guard on `process.type !== 'renderer'` before using the IPC bridge. In the Electron main process (OAuth token exchange) and in the inso CLI (Node.js), hooks are applied directly via `plugins.getRequestHooks()` / `plugins.getResponseHooks()` without any IPC.

Intentionally remaining in renderer

  • `applyColorScheme` / `getColorScheme` — CSS/DOM manipulation, not plugin execution
  • `createPlugin` — filesystem plugin scaffolding, not plugin execution

Security fixes (from review)

  • `plugin-window-ready` and `plugin-invoke-result` validate `event.sender === pluginWindow.webContents`
  • Persistent `ipcMain.on` listener (registered once) replaces `ipcMain.once` — plugin window re-arms correctly after reload
  • `waitForReady()` has a 10s timeout and listens to `did-fail-load`
  • Request IDs use `crypto.randomUUID()` instead of a guessable counter
  • Non-Error plugin hook throws are wrapped into `Error` instances before attaching `.plugin` metadata

Playwright fixture

The plugin window is created after the main window's `did-finish-load` event, so Playwright's `firstWindow()` always returns the main app window. No fixture fallback or polling is needed.

Test plan

  • All 1900 unit tests pass (`npm test -w packages/insomnia`)
  • TypeScript: no errors (`tsc --noEmit`)
  • E2E: `plugin-bridge.test.ts` — writes a test plugin, reloads via bridge, verifies the request action appears in the dropdown (16.7s, passes)
  • Manual: themes settings panel — themes should populate
  • Manual: reload plugins via keyboard shortcut — should complete without error
  • Manual: plugin settings tab — installed plugins should list
  • Manual: right-click a request — plugin actions should appear and execute
  • Manual: cloud credentials settings — Azure auth flow should work

🤖 Generated with Claude Code

closes INS-2466

@jackkav jackkav changed the title Chore:move plugins to hidden window feat: move plugin loading to dedicated hidden BrowserWindow (Phase 1) May 4, 2026
@jackkav jackkav force-pushed the chore/move-plugins-to-hidden-window branch from 2223c13 to 6efa211 Compare May 4, 2026 07:25
@jackkav jackkav marked this pull request as ready for review May 4, 2026 07:25
Copilot AI review requested due to automatic review settings May 4, 2026 07:25
Comment thread packages/insomnia/src/main/plugin-window.ts
Comment thread packages/insomnia/src/main/plugin-window.ts Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR moves plugin enumeration/loading for the UI renderer behind a dedicated hidden Electron BrowserWindow (with Node integration) and routes renderer-side plugin interactions through a typed IPC bridge, as the first phase toward renderer hardening.

Changes:

  • Added a hidden “plugin window” (HTML + entry + preload) and a main-process manager that proxies plugin API calls over IPC.
  • Updated UI codepaths (themes, plugin reload, settings plugins list) to call window.main.plugins.* instead of importing the plugin runtime directly.
  • Introduced serializable bridge types plus new unit tests covering plugin behaviors and plugin hook handling; updated build/esbuild entrypoints to ship the new window scripts.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/insomnia/src/ui/renderer-listeners.ts Route plugin reload through window.main.plugins.reloadPlugins()
packages/insomnia/src/ui/hooks/use-global-keyboard-shortcuts.ts Route plugin reload shortcut through the plugins bridge
packages/insomnia/src/ui/hooks/theme.ts Fetch themes via plugins bridge instead of direct plugin API call
packages/insomnia/src/ui/components/settings/plugins.tsx Use SerializablePlugin and fetch plugin list via bridge
packages/insomnia/src/root.tsx Reload plugins via bridge during theme install/apply flow
packages/insomnia/src/plugins/index.ts Add test helper to control in-memory plugin list
packages/insomnia/src/plugins/bridge-types.ts Define serializable plugin/theme/action metadata boundary + bridge API
packages/insomnia/src/plugins/tests/themes.test.ts Add baseline tests for built-in theme list + plugin theme merge behavior
packages/insomnia/src/plugins/tests/index.test.ts Add broad unit tests for plugin exports (hooks/actions/tags/themes) and errors
packages/insomnia/src/plugin-window.html Add HTML host for hidden plugin window script
packages/insomnia/src/network/network.ts Export internal hook-application helpers for unit testing
packages/insomnia/src/network/tests/plugin-hooks.test.ts Add unit tests around request/response plugin hook behavior
packages/insomnia/src/main/window-utils.ts Create plugin window during window initialization; export destroy helper
packages/insomnia/src/main/plugin-window.ts Implement plugin window lifecycle + IPC proxying + pending request map
packages/insomnia/src/main/ipc/main.ts Add plugins bridge type to main IPC surface; register plugin IPC handlers
packages/insomnia/src/main/ipc/electron.ts Extend IPC channel type unions for new plugin channels
packages/insomnia/src/entry.preload.ts Expose window.main.plugins.* bridge methods in renderer preload
packages/insomnia/src/entry.plugin-window.ts Implement plugin-window-side dispatcher and result serialization
packages/insomnia/src/entry.plugin-window-preload.ts Provide minimal window.app shim to support plugin path resolution
packages/insomnia/scripts/build.ts Copy plugin-window HTML into production build output
packages/insomnia/esbuild.entrypoints.ts Add esbuild entrypoints/watchers for plugin window + preload bundles
packages/insomnia/PLUGIN_SYSTEM_POC.md Add architecture/plan documentation for the plugin system POC
packages/insomnia-smoke-test/playwright.config.ts Change local reporter configuration
AGENTS.md Add guidance on minimizing command output and cx navigation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/insomnia/src/main/plugin-window.ts Outdated
Comment thread packages/insomnia/src/main/plugin-window.ts Outdated
Comment thread packages/insomnia/src/main/plugin-window.ts
Comment thread packages/insomnia/src/network/__tests__/plugin-hooks.test.ts Outdated
Comment thread packages/insomnia/PLUGIN_SYSTEM_POC.md Outdated
Comment thread packages/insomnia/src/main/plugin-window.ts Outdated
Comment thread packages/insomnia/src/main/plugin-window.ts
@jackkav jackkav requested review from a team and ZxBing0066 May 4, 2026 19:11
@jackkav jackkav force-pushed the chore/move-plugins-to-hidden-window branch from d01ff18 to 2835bce Compare May 4, 2026 19:11
@jackkav jackkav requested a review from gatzjames May 4, 2026 19:59
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clean 👍🏻

Comment thread packages/insomnia/src/main/ipc/electron.ts Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to keep this in Confluence

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its easier for the llm to reference here. I'll get rid of it once the work is done though and can add it to confluence for posterity.

ryan-willis
ryan-willis previously approved these changes May 5, 2026
@ZxBing0066
Copy link
Copy Markdown
Member

ZxBing0066 commented May 6, 2026

Some plugin APIs that rely on the main window (e.g., showAlert, showWrapper) will trigger errors following this change. These APIs may require bridging to the main window to maintain functionality.

And if the previous plugins depend on some of the window APIs, like height and width, they also break. As well as if the plugins are trying to append DOMs. Is this acceptable?

@jackkav
Copy link
Copy Markdown
Contributor Author

jackkav commented May 6, 2026

feedback concern:
chome web apis that show UI will need bridges, or be excluded from supported features.
await window.showOpenFilePicker();

templating now runs in main, which means templating has access to nodejs again, temporarily.

injecting custom UI, in to the DOM won't work anymore, because it was using ambiant renderer libraries like React. We could add these to the plugin context and build bridges but this would be securable.

@jackkav jackkav force-pushed the chore/move-plugins-to-hidden-window branch from f985e6b to d472861 Compare May 6, 2026 10:13
@jackkav jackkav force-pushed the chore/move-plugins-to-hidden-window branch from d472861 to 7784ef7 Compare May 18, 2026 09:56
jackkav and others added 10 commits May 20, 2026 10:50
- Set npm loglevel=warn to suppress install/run progress noise
- Switch Playwright local reporter from list to dot (less output per test, CI unchanged)
- Add scripts/setup.sh for one-time local git config (compact log, short status)
- Document setup script in AGENTS.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Revert loglevel=warn from .npmrc — too broad, suppresses CI output
- Remove shell alias suggestions from setup.sh — out of scope for a repo script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes setup.sh in favour of explicit quiet-command guidance that
benefits all agents (Claude, Copilot, Codex) without requiring a
one-time setup step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cx gives agents a cost ladder (overview → symbols → definition → read)
that reduces file reads for all agents that read AGENTS.md — complementary
to CodeGraph which is Claude Code-specific.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All plugin API calls (getThemes, getPlugins, getActivePlugins, reloadPlugins,
getRequestActions, getRequestGroupActions, getWorkspaceActions, getDocumentActions)
are now routed through a dedicated hidden BrowserWindow with nodeIntegration:true
instead of running directly in the renderer.

IPC relay: renderer → ipcMain.handle → plugin window webContents → ipcRenderer.send
back to main → resolve renderer promise via pending-request map with 30s timeout.

Renderer-side callers updated to use window.main.plugins.* bridge.
Two new esbuild entry points added (plugin-window, plugin-window-preload).
Dev build threshold updated from 3 to 6 to account for all contexts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jackkav and others added 19 commits May 20, 2026 10:50
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Moves requestHooks and responseHooks execution into the hidden plugin
window via the IPC bridge. The default-headers built-in runs in the
renderer (no IPC). A cached hasRequestHooks/hasResponseHooks check in
the main process avoids any plugin window round-trip per request when no
user plugins have hooks registered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_applyRequestPluginHooks and _applyResponsePluginHooks now use
window.main.plugins.* IPC only in the Electron renderer. In the main
process (OAuth2 token exchange via get-token.ts) and Node.js CLI
(insomnia-inso), they fall back to loading plugins directly via
plugins.getRequestHooks/getResponseHooks. This fixes:

- inso CLI: "window is not defined" in all run collection/test commands
- Electron: OAuth2 token exchange failing with "no access token provided"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Playwright's firstWindow() was racing with the plugin window and
sometimes returning it instead of the main app window. By deferring
createPluginWindow() to did-finish-load on the main window, the plugin
window is guaranteed to not exist yet when firstWindow() resolves.

Removes the findMainWindow polling fallback from the test fixture.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…textMenuTag type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dler

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gin window to main renderer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ged app crash

The preload was statically importing invokePluginMethod which pulled the
entire plugin system (network stack, NeDB, plugin contexts) into the
preload bundle. In production the bundle is built fresh from source,
causing a module-level crash before window.main is set — breaking the
critical backup smoke test with "Cannot read properties of undefined
(reading 'secretStorage')".

Move the INSOMNIA_ENABLE_PLUGIN_BRIDGE killswitch into a new
renderer-bridge.ts module that lives in the Vite renderer bundle where
those deps already exist. The preload now always uses IPC for all plugin
calls. All window.main.plugins.* call sites updated to use the bridge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jackkav jackkav force-pushed the chore/move-plugins-to-hidden-window branch from 01770b5 to 1292547 Compare May 20, 2026 08:50
| 'plugins.getThemes'
| 'plugins.getWorkspaceActions'
| 'plugins.reloadPlugins'
| 'plugin-ui-prompt'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the plugin channels' naming style are inconsistent.

Comment thread AGENTS.md

Prefer cx over reading files. Escalate: overview → symbols → definition/references → Read tool.

### Quick reference
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems more like a skill rather than an agent instruction. How do you think?

import type { Settings, UserSession } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
import { executePluginMainAction, reloadPlugins } from '~/plugins';
import { executePluginMainAction } from '~/plugins';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call executePluginMainAction from the bridge here?

hasResponseHooks: () => invokePluginBridgeMethod('hasResponseHooks'),
applyRequestHooks: (args: ApplyRequestHooksArgs) => invokePluginBridgeMethod('applyRequestHooks', args),
applyResponseHooks: (args: ApplyResponseHooksArgs) => invokePluginBridgeMethod('applyResponseHooks', args),
getBridgeMetrics: () => invokeWithNormalizedError('plugins.getBridgeMetrics'),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use invokePluginBridgeMethod here instead of invokeWithNormalizedError here?

@jackkav jackkav enabled auto-merge (squash) May 20, 2026 09:28
});

ipcMain.on('plugin-ui-prompt-result', (_event, { id, value }: { id: string; value: string | null }) => {
const resolve = promptPendingRequests.get(id);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No sender check here as other handlers, is it missing?

(async () => {
try {
await initDatabase(pluginWindowDatabase);
initServices(servicesNodeImpl);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to use servicesProxy instead of the servicesNodeImpl to avoid unnecessary imports to the hidden window.

@jackkav jackkav merged commit 9d1dcce into Kong:develop May 20, 2026
17 checks passed
@jackkav jackkav deleted the chore/move-plugins-to-hidden-window branch May 20, 2026 10:01
jackkav added a commit that referenced this pull request May 21, 2026
Rename plugin-invoke → plugins.invoke to match the plugins.* dot-notation
used by all other plugin IPC channels introduced in PR #9889.

Also add the missing plugin handle channels to the HandleChannels type
union in electron.ts (getBridgeMetrics, hasRequestHooks, hasResponseHooks,
applyRequestHooks, applyResponseHooks).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jackkav added a commit that referenced this pull request May 21, 2026
* fix: address plugin bridge review comments from PR #9889

- rename plugin-ui-* IPC channels to plugins.ui* for naming consistency
- add sender validation to plugins.uiPromptResult handler
- use invokePluginBridgeMethod for getBridgeMetrics (adds it to PluginInvokeMethod)
- use window.main.plugins.executePluginMainAction in root.tsx instead of direct import
- use servicesProxy instead of servicesNodeImpl in plugin window entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address Copilot review comments on plugin bridge PR

- Remove getBridgeMetrics from PluginInvokeMethod since it is handled
  by the main process directly and has no case in invokePluginMethod()
- Route getBridgeMetrics in preload via ipcRenderer.invoke directly
- Use plugins.executePluginMainAction in root.tsx to respect the
  INSOMNIA_ENABLE_PLUGIN_BRIDGE rollback switch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feedback

* fix: normalize plugin IPC channel naming and complete HandleChannels

Rename plugin-invoke → plugins.invoke to match the plugins.* dot-notation
used by all other plugin IPC channels introduced in PR #9889.

Also add the missing plugin handle channels to the HandleChannels type
union in electron.ts (getBridgeMetrics, hasRequestHooks, hasResponseHooks,
applyRequestHooks, applyResponseHooks).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* use invoke helper

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants